SOLID Principle

SOLID
단일 책임 원칙(Single Responsibility Principle, SRP)
열림-닫힘 원칙(Open-Closed Principle, OCP)
리스코프 치환 원칙(Liskov Substitution Principle, LSP)
인터페이스 분리 원칙(Interface Segregation Principle, ISP)
의존석 역전 원칙(Dependency Inversion Principle, DIP)
- 단일 책임 원칙(Single Responsibility Principle, SRP)
struct Journal{
string title;
vector<string> entries;
explicit Journal(const string& title): title{title} {}
};
void Journal::add(const string& entry){
static int count=1;
entries.push_back(boost::lexical_cast<string>(count++)+": "+entry);
}
//
Journal j{"Dear Diary"};
j.add("I cried today");
j.add("I ate a bug");
Journal을 사용함에 있어서 책임을 완수하기 위한 함수는 Journal에 포함되는 것이 공정하다.
디스크에 영구적으로 파일을 저장하기 위한 Journal::save 함수
void Journal::save(const string& filename){
ofstream ofs(filename);
for(auto& s: entries) ofs<<s<<endl;
}
위처럼 디스크의 스트림(ofstream)을 사용해야 하는 경우,
메모장의 책임은 메모 항목을 기입/관리하는 것이지 디스크에 쓰는 것이 아니다.

만일 디스크에 파일을 쓰는 기능도 Journal이 책임지도록 하는 경우,
데이터 저장 방식이 바뀔 때마다 클래스의 코드를 모두 수정해야 한다.
( 작은 수정으로 여러 클래스를 수정해야 하는 경우, 아키텍처에 문제가 있는 것임 “코드 스멜(code smell)” )

파일 저장 기능(자원을 사용하는 기능)은 메모장과 별도로 취급하여 별도의 클래스로 만드는 것이 바람직함
struct PersistenceManagement{
static void save(const Journal& j, const string& filename){
ofstream ofs(filenmae);
for(auto& s: j.entries) ofs<<s<<endl;
}
};
각 클래스는 단 한 가지 책임을 부여받아, 수정할 이유가 단 한가지여야 한다.
- 열림-닫힘 원칙(Open-Closed Principle, OCP)
enum class Color{ Red, Green, Blue };
enum class Size{ Small, Medium, Large };
struct Product{
string name;
Color color;
Size size;
};
struct ProductFilter{
typedef vector<Product*> Items;
};
ProductFilter::Items ProductFilter::by_color(Items items, Color color){
Items result;
for(auto& i: items){
if(i->color==color) result.push_back(i);
}
return result;
}
만일 위와 같이 구현한 후, 크기를 기준으로 한 필터링 기능 혹은 색상과 크기를 모두 지정해서
필터링하는 기능을 추가하고 싶은 경우,
ProductFilter.cpp을 찾아서 아래와 같이 두 함수를 추가해 주어야 한다.
PorductFilter::Items ProductFilter::by_size(Items items, Size size){
Items result;
for(auto& i: items){
if(i->size==size) result.push_back(i);
}
return result;
}
ProductFilter::Items ProductFilter::by_color_and_size(Items items, Size size, Color color){
Items result;
for(auto& i: items){
if(i->size==size && i->color==color) result.push_back(i);
}
return result;
}
열림-닫힘 원칙은 타입이 확장에는 열려 있지만, 수정에는 닫혀 있도록 강제하는 것을 뜻한다.
(기존의 코드 수정 없이, 필터링을 확장할 수 있는 방법)

따로 컴파일되는 모듈을 생성할 것
SRP 원칙에 따라 위의 필터링 절차를 구분한다.
1. 필터
2. 명세
//
template <typename T> struct Specification{
virtual vool is_satisfied(T* item)=0;
};
//
template <typename T> struct Filter{
virtual vector<T*> filter(vector<T*> items, Specification<T>& spec)=0;
};
filter 함수는 전체 항목 집합(items)와 명세(spec)을 인자로 받아,
명세에 합치되는 항목을 리턴하는 함수
struct BetterFilter: Filter<Prodcut>{
vector<Product*> filter(vector<Porduct*> items, Specification<Product>& spec) override {
vector<Product*> result;
for(auto& p: items) if(spec.is_satisfied(p)) result.push_back(p);
return result;
}
};
struct ColorSpecification: Specification<Product>{
Color color;
explicit ColorSpecification(const Color color): color{color} {}
bool is_satisfied(Product* item) override {
return item->color==color;
}
};
//
Product apple{"Apple", Color::Green, Size::Small};
Product tree{"Tree", Color::Green, Size::Large};
Product house{"House", Color::Blue, Size::Large};
vector<Product*> all{&apple, &tree, &house};
BetterFilter bf;
ColorSpecification green(Color::Green);
auto green_things=bf.filter(all, green);
for(auto& x: green_things) cout<<x->name<<" is green"<<endl;
    AND를 이용한 복합(composite) 명세
template <typename T> struct AndSpecification: Specification<T>{
Specification<T>& first;
Specification<T>& second;
AndSpecification(Specification<T>& first, Specification<T>& second): first{first}, second{second} {}
bool is_satisfied(T* item) override{
return first. is_satisfied(item)&&second.is_satisfied(item);
}
};
//
SizeSpecification large(Size::Large);
ColorSpecification green(Color::Green);
AndSpecification<Product> green_and_large{ large, green };
auto big_green_things=bf.filter(all, green_and_big);
for(auto& x: big_green_things) cout<<x-x>name<<" is large and green"<<endl;
    && 연산자 오버로딩을 이용한 복합(composite) 명세
template <typename T> struct Specification{
virtual bool is_satisfied(T* item)=0;
AndSpecification<T> operator&&(Specification&& other){
return AndSpecification<T>(*this, other);
}
};
//
auto green_and_big=ColorSpecification(Color::green)&&SizeSpecification(Size::Large); // SizeSpecification(Rvalue)
OCP는 기존에 작성하고 테스트 했던 코드에 다시 수정하는 것을 허용하지 않음을 의미한다.
인터페이스 자체는 수정하지 않고, 인터페이스 구현을 통해 새로운 필터링 방식을 추가함
“확장에는 열려 있지만, 수정에는 닫혀있다."
- 리스코프 치환 원칙(Liskov Substitution Principle, LSP)
리스코프 치환 원칙은 자식 객체에 접근할 때, 부모 객체의 인터페이스로 접근하는데 문제가 없어야 함을 의미한다.
자식 객체를 부모 객체와 동일하게 취급할 수 있어야 함
// LSP
class Rectangle{
protected:
int width, height;
public:
Rectangle(const int width, const int height): width{width}, height{height} {}
int get_width() const { return width; }
virtual void set_width(const int width) { this->width=width; }
int get_height() const { return height; }
virtual void set_height(const int height){ this->height=height; }
int area() const { return width*height; }
};
class Square: public Rectangle{
public:
Square(int size): Rectangle(size, size){}
void set_width(const int width) override{
this->width=height=width;
}
void set_height(const int height) override{
this->height=width=height;
}
};
void process(Rectangle& r){
int w=r.get_width();
r.set_height(10);
std::cout<<"expected area= "<<(w*10)<<", got "<<r.area()<<std::endl;
}
//
Square s{5};
process(s); // , .
virtual로 정의된 함수는 다운캐스팅하여 메소드를 호출해도, 원래 타입에서 오버라이딩된 메서드를 호출한다.
위에서 r.set_height에서 Rectangle 타입의 r을 호출하였지만,
내부적으로 Square::set_height가 호출된다.
LSP를 준수하기 위해 서브클래스 대신에 Factory 클래스를 두어
직사각형과 정사각형 객체를 따로 생성할 수 있다.
struct RectangleFactory{
static Rectangle create_rectangle(int w, int h);
static Rectangle create_square(int size);
};
bool Rectangle::is_square() const{
return width==height;
}
- 인터페이스 분리 원칙(Interface Segregation Principle, ISP)
struct MyFavouritePrinter: IMachine{
void print(vector<Document*> docs) override;
void fax(vector<Document*> docs) override;
void scan(vector<Document*> docs) override;
};
struct IMachine{ //
virtual void print(vector<Document*> docs)=0;
virtual void fax(vector<Document*> docs)=0;
virtual void scan(vector<Document*> docs)=0;
};
위와 같이 인터페이스는 print, fax, scan을 모든 기능을 구현하도록 강제한다.
구현하지 않기 위해서는 빈 함수를 만들어서 구현한 것처럼 적어주어야 함

인터페이스 분리 원칙은 필요에 따라 구현할 대상을 선별할 수 있도록 인터페이스를 별개로 둠을 의미한다.
struct IPrinter{
virtual void print(vector<Document*> docs=0;
};
struct IScanner{
virtual void scan(vector<Document*> docs=0;
};
struct Printer: IPrinter{
void print(vector<Document*> docs) override;
}
struct Scanner: IScanner{
void scan(vector<Document*> docs) override;
};
//
struct IMachine: IPrinter, IScanner{
// ...
};
// IPrinter, IScanner
struct Machine: IMachine{
IPrinter& printer;
IScanner& scanner;
Machine(IPrinter& printer, IScanner& scanner): printer{printer}, scanner{scanner} {}
void print(vector<Document*> docs) override{
printer.print(docs);
}
void scan(vector<Document*> docs) override{
scanner.scan(docs);
}
};
인터페이스를 모든 항목에 대해 강제하지 않고, 실제 필요한 인터페이스만 구현할 수 있도록 함
- 의존성 역전 원칙(Dependency Inversion Principle, DIP)
A. 상위 모듈이 하위 모듈에 종속성을 가져서는 안 된다. 양쪽 모두 추상화에 의존해야 한다.
B. 추상화가 세부 사항에 의존해서는 안 된다. 세부 사항이 추상화에 의존해야 한다.

A:
예를 들어 로깅 기능(하위 모듈)에서 로그 리포팅 컴포넌트(상위 모듈)가 실구현체인 ConsoleLogger에
의존해서는 안된다.
ILogger 인터페이스에만 의존해야 한다.
B:
종속성이 실 구현 타입이 아닌 인터페이스 혹은 부모 클래스에 있어야 한다.
class Reporting{ //
ILogger& logger;
public:
Reporting(const ILogger& logger): logger{logger} {}
void prepare_report(){
logger.log_info("Preparing the report");
// ...
}
};
하지만 위와 같이 인터페이스 타입을 포함한 클래스를 인스턴스화하기 위해서는
구현 클래스를 호출해야 한다. Reporting{ConsoleLogger{}} 등
    종속성 주입(Dependency injection) 테크닉
Boost.DI와 같은 라이브러리를 이용해 컴포넌트의 종속성 요건을 자동으로 만족하게 함
// Engine
struct Engine{
float volume=5;
int horse_power=400;
friend ostream& operator<<(ostream& os, const Engin& obj){
return os<<"volume: "<<obj.volume<<" horse_power: "<<obj.horse_power;
}
};
// Log
struct ILogger{
virtual ~ILogger() {}
virtual void Log(const string& s)=0;
}; //
struct ConsoleLogger: ILogger{
ConsoleLogger() {}
void Log(const string& s) override {
cout<<"LOG: "<<s.c_str()<<endl;
}
};
//
struct Car{
unique_ptr<Engine> engine; //
shared_ptr<ILogger> logger;
Car(unique_ptr<Engine> engine, const shared_ptr<ILogger>& logger): engine{move(engine)}, logger{logger} {
logger->Log("making a car");
}
friend ostream& operator<<(ostream& os, const Car& obj){
return os<<"car with engine: "<<*obj.engine;
}
};
Car 생성자는 make_unique/make_shared의 호출이 있으리라 가정한다(종속성)
종속성 주입에서는 Boost.DI를 이용한다.
ILogger를 ConsolerLogger에 연결하는 bind를 정의한다.
(ILogger를 요청하면, ConsolerLogger를 전달)
auto injector=di::make_injector(di::bind<ILogger>().to<ConsoleLogger>());
auto car=injector.create<shared_ptr<Car>>();
위와 같이 종속성 주입으로 코드를 작성하면,
ILogger 인스턴스 타입이 수정될 때, bind가 수행되는 부분만 수행되면
자동으로 ILogger를 사용하는 모든 곳에 적용된다.